const scratchCenter = new Cesium.Cartesian4();
const scratchClip = new Cesium.Cartesian4();
const scratchCartesian3 = new Cesium.Cartesian3();
const Cartesian3Tmp = new Cesium.Cartesian3();
const scratchCartesian2 = new Cesium.Cartesian2();
const HEIGHTLIMIT = 30;
const MINHEIGHTLIMIT = 5;
const DISTANCELIMIT = 1000;
const MAXDISTANCELIMIT = DISTANCELIMIT * 10;

const BEIDOUGRID = {
    ID: 'BEIDOUGRID',
    ROWS: [2, 22, 8, 3, 10, 15, 2, 8, 8, 8, 8],
    COLS: [1, 60, 12, 2, 15, 15, 2, 8, 8, 8, 8],
    CODE: [-1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1],
    HEIGHT: [40075020, 445280, 55660, 27830, 1850, 123.69, 61.84, 7.73, 0.97, 0.121, 0.015],
    RECTANGLE: [-180, -88, 180, 88],
    TWIDTH: Math.PI * 2.0,
    THEIGHT: 176 * Math.PI / 180.0
}

const FLYPATHCOORDS = [
    118.79837320681966, 32.03680458238358, 11,
    118.79837320681966, 32.03680458238358, 130.5,
    118.79966314054167, 32.03705195429375, 130.5,
    118.79966314054167, 32.03705195429375, 180,
    118.79844557774508, 32.03792627976566, 180,
    118.79844557774508, 32.03792627976566, 11
];

function TilesRectangle(tileset) {
    const bs = tileset.root.boundingSphere;
    const rect = Cesium.Rectangle.fromBoundingSphere(bs);
    return rect;
}

function TestRect() {
    return Cesium.Rectangle.fromDegrees(
        118.7980326707774,
        32.03620749609744,
        118.80025138702175,
        32.03795726023944
    )
}

function TestFlyPath() {
    // fromDegreesArrayHeights
    const polyline = new Cesium.PolylineGeometry({
        positions: Cesium.Cartesian3.fromDegreesArrayHeights(FLYPATHCOORDS),
        width: 2
    });
    const geom = Cesium.PolylineGeometry.createGeometry(polyline);
    const ins = new Cesium.GeometryInstance({
        geometry: geom
    });
    const primitive = new Cesium.Primitive({
        geometryInstances: [ins],
        appearance: new Cesium.PolylineMaterialAppearance({
            material: Cesium.Material.fromType("Color", { color: new Cesium.Color(0, 0, 1, 1) })
        }),
        allowPicking: false
    });
    return primitive;
}

function projectOBB(obb, axis) {
    let min = Infinity, max = -Infinity;
    for (let i = -1; i <= 1; i += 2) {
        for (let j = -1; j <= 1; j += 2) {
            for (let k = -1; k <= 1; k += 2) {
                const corner = {
                    x: obb.center.x + i * obb.halfAxes[0] + j * obb.halfAxes[3] + k * obb.halfAxes[6],
                    y: obb.center.y + i * obb.halfAxes[1] + j * obb.halfAxes[4] + k * obb.halfAxes[7],
                    z: obb.center.z + i * obb.halfAxes[2] + j * obb.halfAxes[5] + k * obb.halfAxes[8]
                }
                const projection = dot(corner, axis);
                min = Math.min(min, projection);
                max = Math.max(max, projection);
            }
        }
    }
    return { min, max }
}
function dot(v1, v2) {
    return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
}
function cross(v1, v2) {
    return {
        x: v1.y * v2.z - v1.z * v2.y,
        y: v1.z * v2.x - v1.x * v2.z,
        z: v1.x * v2.y - v1.y * v2.x
    }
}
function normalize(v) {
    const magnitude = Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
    return {
        x: v.x / magnitude,
        y: v.y / magnitude,
        z: v.z / magnitude
    }
}
Cesium.OrientedBoundingBox.prototype.Intersect = function (obb2) {
    const obb1 = this;
    const obb1x = { x: obb1.halfAxes[0], y: obb1.halfAxes[1], z: obb1.halfAxes[2] };
    const obb1y = { x: obb1.halfAxes[3], y: obb1.halfAxes[4], z: obb1.halfAxes[5] };
    const obb1z = { x: obb1.halfAxes[6], y: obb1.halfAxes[7], z: obb1.halfAxes[8] };
    const obb2x = { x: obb2.halfAxes[0], y: obb2.halfAxes[1], z: obb2.halfAxes[2] };
    const obb2y = { x: obb2.halfAxes[3], y: obb2.halfAxes[4], z: obb2.halfAxes[5] };
    const obb2z = { x: obb2.halfAxes[6], y: obb2.halfAxes[7], z: obb2.halfAxes[8] };
    const axes = [
        obb1x, obb1y, obb1z,
        obb2x, obb2y, obb2z,
        cross(obb1x, obb2x), cross(obb1x, obb2y), cross(obb1x, obb2z),
        cross(obb1y, obb2x), cross(obb1y, obb2y), cross(obb1y, obb2z),
        cross(obb1z, obb2x), cross(obb1z, obb2y), cross(obb1z, obb2z)
    ];
    for (const axis of axes) {
        const normalizedAxis = normalize(axis);
        const projection1 = projectOBB(obb1, normalizedAxis);
        const projection2 = projectOBB(obb2, normalizedAxis);
        if (projection1.max < projection2.min || projection2.max < projection1.min) {
            return false;
        }
    }
    return true;
}

Cesium.OrientedBoundingBox.prototype.IntersectLine = function (linecoords) {
    if (!this.inverseOrientation) {
        // no scale for any coords, so we need normalize all half axes
        const halfx = Cesium.Matrix3.getColumn(this.halfAxes, 0, new Cesium.Cartesian3());
        const halfy = Cesium.Matrix3.getColumn(this.halfAxes, 1, new Cesium.Cartesian3());
        const halfz = Cesium.Matrix3.getColumn(this.halfAxes, 2, new Cesium.Cartesian3());
        this.axesLength = new Cesium.Cartesian3();
        this.axesLength.x = Cesium.Cartesian3.magnitude(halfx);
        this.axesLength.y = Cesium.Cartesian3.magnitude(halfy);
        this.axesLength.z = Cesium.Cartesian3.magnitude(halfz);
        Cesium.Cartesian3.normalize(halfx, halfx);
        Cesium.Cartesian3.normalize(halfy, halfy);
        Cesium.Cartesian3.normalize(halfz, halfz);
        const rotation = new Cesium.Matrix3();
        Cesium.Matrix3.setColumn(rotation, 0, halfx, rotation);
        Cesium.Matrix3.setColumn(rotation, 1, halfy, rotation);
        Cesium.Matrix3.setColumn(rotation, 2, halfz, rotation);
        const translation = this.center;
        const tmp = new Cesium.Matrix4();
        this.inverseOrientation = new Cesium.Matrix4();
        Cesium.Matrix4.fromRotationTranslation(rotation, translation, tmp);
        Cesium.Matrix4.inverse(tmp, this.inverseOrientation);
    }
    for (let i = 0; i < linecoords.length - 1; i++) {
        const pt1 = linecoords[i];
        const pt2 = linecoords[i + 1];

        const starttoobb = new Cesium.Cartesian4(pt1.x, pt1.y, pt1.z, 1);
        const endtoobb = new Cesium.Cartesian4(pt2.x, pt2.y, pt2.z, 1);

        const lineStartLocal = new Cesium.Cartesian4();
        const lineEndLocal = new Cesium.Cartesian4();
        Cesium.Matrix4.multiplyByVector(this.inverseOrientation, starttoobb, lineStartLocal);
        Cesium.Matrix4.multiplyByVector(this.inverseOrientation, endtoobb, lineEndLocal);
        Cesium.Cartesian4.multiplyByScalar(lineStartLocal, 1.0 / lineStartLocal.w, lineStartLocal);
        Cesium.Cartesian4.multiplyByScalar(lineEndLocal, 1.0 / lineEndLocal.w, lineEndLocal);
        const lineDirectionLocal = new Cesium.Cartesian4();
        Cesium.Cartesian4.subtract(lineEndLocal, lineStartLocal, lineDirectionLocal);

        const minX = -this.axesLength.x;
        const maxX = this.axesLength.x;
        const minY = -this.axesLength.y;
        const maxY = this.axesLength.y;
        const minZ = -this.axesLength.z;
        const maxZ = this.axesLength.z;

        let tmin = 0;
        let tmax = 1;

        let t1 = (minX - lineStartLocal.x) / lineDirectionLocal.x;
        let t2 = (maxX - lineStartLocal.x) / lineDirectionLocal.x;

        tmin = Math.max(tmin, Math.min(t1, t2));
        tmax = Math.min(tmax, Math.max(t1, t2));

        if (tmax < tmin) {
            continue;
        }

        t1 = (minY - lineStartLocal.y) / lineDirectionLocal.y;
        t2 = (maxY - lineStartLocal.y) / lineDirectionLocal.y;

        tmin = Math.max(tmin, Math.min(t1, t2));
        tmax = Math.min(tmax, Math.max(t1, t2));

        if (tmax < tmin) {
            continue;
        }

        t1 = (minZ - lineStartLocal.z) / lineDirectionLocal.z;
        t2 = (maxZ - lineStartLocal.z) / lineDirectionLocal.z;

        tmin = Math.max(tmin, Math.min(t1, t2));
        tmax = Math.min(tmax, Math.max(t1, t2));

        if (tmax < tmin) {
            continue;
        }

        return tmax > -0.001 && tmax < 1.001;
    }
    return false;
}

class GDGridScheme {
    constructor() {
        const rad = Math.PI / 180.0;
        this._rows = [-1, 22, 8, 3, 10, 15, 2, 8, 8, 8, 8]
        this._cols = [-1, 60, 12, 2, 15, 15, 2, 8, 8, 8, 8]
        this._code = [-1, 2, 1, 0, 1, 1, 0, 1, 1, 1, 1]
        this._codeCount = [1, 3, 2, 1, 2, 2, 1, 2, 2, 2, 2]
        this._deltaLamda = [360 * rad]
        this._deltaFai = [88 * rad]

        const arrLen = 11

        for (let L = 1; L < arrLen; L++) {
            this._deltaLamda.push(this._deltaLamda[L - 1] / this._cols[L]);
            this._deltaFai.push(this._deltaFai[L - 1] / this._rows[L]);
        }
    }

    getResolution(level) {
        return {
            xRes: this._deltaLamda[level],
            yRes: this._deltaFai[level]
        }
    }
}

class GDGrid {
    constructor(viewer) {
        this._viewer = viewer;
        this._level = 8;
        this._scheme = new GDGridScheme();
        this._primitives = new Cesium.PrimitiveCollection();
        this._viewer.scene.primitives.add(this._primitives);
        // this._viewer.camera.moveEnd.addEventListener(GDGrid.prototype.Update, this);
    }

    async loadFlyPath() {
        const self = this;
        const url = "./flypath.json";
        const res = await fetch(url);
        const content = await res.json();
        self._flyPath = content;
        for (const fp of self._flyPath) {
            let west = fp.coords[0], east = fp.coords[0];
            let south = fp.coords[1], north = fp.coords[1];
            for (let i = 3; i < fp.coords.length; i += 3) {
                if (west > fp.coords[i]) west = fp.coords[i];
                if (east < fp.coords[i]) east = fp.coords[i];
                if (south > fp.coords[i + 1]) south = fp.coords[i + 1];
                if (north < fp.coords[i + 1]) north = fp.coords[i + 1];
            }
            fp.rect = Cesium.Rectangle.fromDegrees(west, south, east, north);
        }
    }

    Clear() {
        this._primitives.removeAll();
    }

    getCache(id) {
        const cache = localStorage.getItem(id);
        if (cache) {
            const grid = JSON.parse(cache);
            // the path has been calculated, just draw grid directly
            this.drawGrid(grid.red, grid.green, grid.outline, grid.deltHeight);
            return true;
        }

        return false;
    }

    drawGrid(red, green, outline, deltHeight) {
        function insGrid(rects, inss) {
            for (let i of rects) {
                const rectgeom = new Cesium.RectangleGeometry({
                    ellipsoid: Cesium.Ellipsoid.default,
                    rectangle: Cesium.Rectangle.fromRadians(i[0], i[1], i[2], i[3]),
                    height: i[4],
                    extrudedHeight: i[4] + deltHeight
                });
                const geom = Cesium.RectangleGeometry.createGeometry(rectgeom);
                inss.push(new Cesium.GeometryInstance({
                    geometry: geom
                }));
            }
        }
        const redIns = [];
        const greenIns = [];
        const outlineIns = [];
        insGrid(red, redIns);
        insGrid(green, greenIns);
        for (let i of outline) {
            const rectoutline = new Cesium.RectangleOutlineGeometry({
                ellipsoid: Cesium.Ellipsoid.default,
                rectangle: Cesium.Rectangle.fromRadians(i[0], i[1], i[2], i[3]),
                height: i[4],
                extrudedHeight: i[4] + deltHeight
            });
            const geom = Cesium.RectangleOutlineGeometry.createGeometry(rectoutline);
            outlineIns.push(new Cesium.GeometryInstance({
                geometry: geom
            }));
        }

        const redPri = new Cesium.Primitive({
            geometryInstances: redIns,
            appearance: new Cesium.MaterialAppearance({
                material: Cesium.Material.fromType("Color", { color: new Cesium.Color(1, 0, 0, 0.2) })
            }),
            allowPicking: false,
        });

        const outlinePri = new Cesium.Primitive({
            geometryInstances: outlineIns,
            appearance: new Cesium.MaterialAppearance({
                material: Cesium.Material.fromType("Color", { color: new Cesium.Color(1, 1, 0, 1) })
            }),
            allowPicking: false,
        });

        const greenPri = new Cesium.Primitive({
            geometryInstances: greenIns,
            appearance: new Cesium.MaterialAppearance({
                material: Cesium.Material.fromType("Color", { color: new Cesium.Color(0, 1, 0, 0.2) })
            }),
            allowPicking: false
        });

        this._primitives.add(redPri);
        this._primitives.add(greenPri);
        this._primitives.add(outlinePri);
    }

    drawFlyPath(coords) {
        const polyline = new Cesium.PolylineGeometry({
            positions: coords,
            width: 2
        });
        const geom = Cesium.PolylineGeometry.createGeometry(polyline);
        const ins = new Cesium.GeometryInstance(
            { geometry: geom }
        );
        const primitive = new Cesium.Primitive({
            geometryInstances: [ins],
            appearance: new Cesium.PolylineMaterialAppearance({
                material: Cesium.Material.fromType("Color", { color: new Cesium.Color(0, 0, 1, 1) })
            }),
            allowPicking: false
        });
        this._primitives.add(primitive);
    }

    Add(tileset, idx) {
        tileset.debugFreezeFrame = true;
        const cellRes = this._scheme.getResolution(this._level);
        const deltHeight = BEIDOUGRID.HEIGHT[this._level];
        let curheight = Math.floor(MINHEIGHTLIMIT / deltHeight) * deltHeight;
        // need clamp to the cell
        // when the view of camera is horizontal, the view rectangle is too large
        // need calculate a acceralte viewrectangle
        // const curRect = this._viewer.camera.computeViewRectangle();
        const fp = this._flyPath[idx];
        if (fp.coords.length === 0) {
            tileset.debugFreezeFrame = false;
            return;
        }
        const fp_cartesian = Cesium.Cartesian3.fromDegreesArrayHeights(fp.coords);
        this.drawFlyPath(fp_cartesian);
        // if there has cache, just drwa cache
        const hascache = this.getCache(fp.id);
        if (hascache) {
            tileset.debugFreezeFrame = false;
            return;
        }
        const leftlng = Math.floor(fp.rect.west / cellRes.xRes) * cellRes.xRes;
        const leftlat = Math.floor(fp.rect.south / cellRes.yRes) * cellRes.yRes;
        const rightlng = Math.ceil(fp.rect.east / cellRes.xRes) * cellRes.xRes;
        const rightlat = Math.ceil(fp.rect.north / cellRes.yRes) * cellRes.yRes;

        const cache = {
            red: [],
            green: [],
            outline: [],
            deltHeight
        };

        while (curheight < HEIGHTLIMIT) {
            for (let lng = leftlng; lng < rightlng; lng += cellRes.xRes) {
                for (let lat = leftlat; lat < rightlat; lat += cellRes.yRes) {
                    Cesium.Cartesian3.fromRadians(lng, lat, curheight, Cesium.Ellipsoid.default, scratchCartesian3);
                    // if (!this.gridVisiable(scratchCartesian3)) continue;
                    const rect = Cesium.Rectangle.fromRadians(lng, lat, lng + cellRes.xRes, lat + cellRes.yRes);
                    const obb = Cesium.OrientedBoundingBox.fromRectangle(rect, curheight, curheight + deltHeight);
                    const online = obb.IntersectLine(fp_cartesian);
                    if (online) {
                        cache.green.push([lng, lat, lng + cellRes.xRes, lat + cellRes.yRes, curheight]);
                        cache.outline.push([lng, lat, lng + cellRes.xRes, lat + cellRes.yRes, curheight]);
                    }
                }
            }
            curheight += deltHeight;
        }

        localStorage.setItem(fp.id, JSON.stringify(cache));
        this.drawGrid(cache.red, cache.green, cache.outline, deltHeight);
        tileset.debugFreezeFrame = false;
    }

    gridVisiable(center) {
        scratchCenter.x = center.x;
        scratchCenter.y = center.y;
        scratchCenter.z = center.z;
        scratchCenter.w = 1;

        Cesium.Matrix4.multiplyByVector(
            this._viewer.scene.frameState.context.uniformState.viewProjection,
            scratchCenter,
            scratchClip
        );
        Cesium.Cartesian4.multiplyByScalar(scratchClip, 1 / scratchClip.w, scratchClip);

        return !(scratchClip.x < -1 || scratchClip.x > 1 ||
            scratchClip.y < -1 || scratchClip.y > 1 ||
            scratchClip.z < -1 || scratchClip.z > 1);
    }

    gridMoreThanOnePixel(center, height) {
        const scale = 0.1;
        const position = this._viewer.camera.position;
        const direction = this._viewer.camera.direction;
        const toCenter = Cesium.Cartesian3.subtract(center, position, Cartesian3Tmp);
        const toCenterProj = Cesium.Cartesian3.multiplyByScalar(direction, Cesium.Cartesian3.dot(direction, toCenter), Cartesian3Tmp);
        const distance = Cesium.Cartesian3.magnitude(toCenterProj);
        // if (distance > DISTANCELIMIT * height) {
        //     return false;
        // }
        // else {
        //     return true;
        // }
        const pixelSize = this._viewer.camera.frustum.getPixelDimensions(
            this._viewer.scene.drawingBufferWidth,
            this._viewer.scene.drawingBufferHeight,
            distance,
            this._viewer.scene.pixelRatio,
            scratchCartesian2
        );
        return pixelSize.x < height * scale && pixelSize.y < height * scale;
    }
    /**
     * get approximate view rectangle
     */
    ViewRectangle() {
        // use right of camera, normal of surface, calcualte the fron vector
        // use this axes create a new rectangle for view
        const latlng = Cesium.Cartographic.fromCartesian(this._viewer.camera.position);
        if (latlng.height > DISTANCELIMIT) return null;
        const circleCenter = Cesium.Cartesian3.fromRadians(latlng.longitude, latlng.latitude, 0);
        const geonormal = new Cesium.Cartesian3();
        Cesium.Ellipsoid.WGS84.geodeticSurfaceNormal(circleCenter, geonormal);
        const right = this._viewer.camera.rightWC;
        const front = new Cesium.Cartesian3();
        Cesium.Cartesian3.cross(geonormal, right, front);
        const radius = Math.max(Math.min(latlng.height * 2, MAXDISTANCELIMIT), DISTANCELIMIT);
        const frontoffset = new Cesium.Cartesian3();
        Cesium.Cartesian3.multiplyByScalar(front, radius, frontoffset);
        Cesium.Cartesian3.add(circleCenter, frontoffset, frontoffset);
        const ascpect = this._viewer.canvas.width / this._viewer.canvas.height;
        const wradius = ascpect * radius;
        const horizontaloffset = new Cesium.Cartesian3();
        const rightoffset = new Cesium.Cartesian3();
        Cesium.Cartesian3.multiplyByScalar(right, wradius, horizontaloffset);
        Cesium.Cartesian3.add(circleCenter, horizontaloffset, rightoffset);
        const leftoffset = new Cesium.Cartesian3();
        Cesium.Cartesian3.subtract(circleCenter, horizontaloffset, leftoffset);

        const leftlatlng = Cesium.Cartographic.fromCartesian(leftoffset);
        const rightlatlng = Cesium.Cartographic.fromCartesian(rightoffset);
        const frontlatlng = Cesium.Cartographic.fromCartesian(frontoffset);

        const west = Math.min(latlng.longitude, leftlatlng.longitude, rightlatlng.longitude, frontlatlng.longitude);
        const east = Math.max(latlng.longitude, leftlatlng.longitude, rightlatlng.longitude, frontlatlng.longitude);
        const south = Math.min(latlng.latitude, leftlatlng.latitude, rightlatlng.latitude, frontlatlng.latitude);
        const north = Math.max(latlng.latitude, leftlatlng.latitude, rightlatlng.latitude, frontlatlng.latitude);

        return new Cesium.Rectangle(west, south, east, north);
    }

    UpdateColor() {
        for (let i = 0; i < this._primitive.geometryInstances.length * 0.5; i++) {
            const ins = this._primitive.geometryInstances[i];
            ins.attributes.color.value[0] = 255;
            ins.attributes.color.value[1] = 255;
        }
    }

    queryTilesetIntersect(obb, tileset) {
        for (const tile of tileset._selectedTiles) {
            if (tile.content) {
                const tobb = tile.boundingVolume.boundingVolume;
                const intersected = obb.Intersect(tobb);
                if (intersected) {
                    return true;
                }
            }
        }
        return false;
    }
}

export { GDGrid, GDGridScheme, BEIDOUGRID, TilesRectangle, TestRect, TestFlyPath }